/*
 * $Id: QELogDocument.m 101 2009-12-20 19:35:28Z jon@bway.net $
 * Chalk
 *
 * Created 10/5/08
 * Copyright Jon Gordon
 * 
 * Permission is hereby granted, free of charge, to any person obtaining 
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to 
 * permit persons to whom the Software is furnished to do so, subject to 
 * the following conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#import "QELogDocument.h"
#import "QELogWindowController.h"
#import "QEADIFImporter.h"
#import "QSO.h"

#include <float.h>
#include <math.h>

#import "QEADIF.h"


#define MAX_MERGE_SECONDS ((double) 300.0)

static NSSet *qslColumns = nil;


@interface QELogDocument (private)
- (void)doADIFExport;
- (void)doADIFImport;
- (BOOL)mergeRecord:(NSDictionary *)record matches:(NSMutableSet *)matches;
@end


@implementation QELogDocument
@synthesize mergeRequested;

#pragma mark LIFECYCLE METHODS
+ (void)initialize
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    static BOOL initialized = NO;
    
    if (!initialized) {
        NSLog(@"  initializing");
        initialized = YES;
        qslColumns = [[NSSet alloc] initWithObjects:@"qsl_lotw_rcvd", @"qsl_lotw_sent",
                      @"qsl_sent", @"qsl_rcvd", nil];
    } // if
} // initialize

- (id)init 
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    self = [super init];
    if (self != nil) {
        // initialization code
    }
    return self;
} // init

- (void)makeWindowControllers
{
    logWindowController = [[QELogWindowController alloc] init];
    [self addWindowController:logWindowController];
    [logWindowController release];
} // makeWindowControllers


#pragma mark ACTIONS
- (IBAction)adifExport:(id)sender
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
	[self doADIFExport];
} // adifExport:

- (IBAction)adifImport:(id)sender
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
	[self doADIFImport];
} // adifImport:

- (IBAction)delete:(id)sender
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    NSManagedObjectContext *moc = [self managedObjectContext];
    
    for (QSO *qso in [[logWindowController qsosController] selectedObjects]) {
        [moc deleteObject:qso];
    } // for
} // delete:


#pragma mark OPERATIONS
- (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    SEL action = [anItem action];
    
    if (action == @selector(delete:)) {
        return 0 < [[[logWindowController qsosController] selectedObjects] count];
    } // if
    
    return [super validateUserInterfaceItem:anItem];
} // validateUserInterfaceItem:


- (void)doADIFExport
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    int panelResult;
    NSSavePanel *sPanel = [NSSavePanel savePanel];
    
    [sPanel setRequiredFileType:@"adi"];
    [sPanel setAccessoryView:logWindowController.exportPanelView];
    
    // enable or disable the export options, as appropriate
    NSMatrix *typeMatrix = logWindowController.exportTypeSelector;
    NSInteger nRows = [typeMatrix numberOfRows];
    NSInteger nCols = [typeMatrix numberOfColumns];
    NSInteger rowIdx, colIdx;
    NSArray *noLotwQSOs;
    
    for (rowIdx = 0; rowIdx < nRows; ++rowIdx) {
        for (colIdx = 0; colIdx < nCols; ++colIdx) {
            id cell = [typeMatrix cellAtRow:rowIdx column:colIdx];
            if (cell == logWindowController.exportAllButton) {
                [cell setEnabled:YES];
                [typeMatrix selectCellAtRow:rowIdx column:colIdx];
            } else if (cell == logWindowController.exportSelectedButton) {
                [cell setEnabled:(0 < [[[logWindowController qsosController] selectedObjects] count])];
            } else if (cell == logWindowController.exportLoTWButton) {
                NSManagedObjectContext *moc = [self managedObjectContext];
                NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"QSO"
                                                                     inManagedObjectContext:moc];
                NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
                [request setEntity:entityDescription];
                NSPredicate *qsoPredicate = [NSPredicate 
                                             predicateWithFormat:
                                             @"qsl_lotw_sent == NO"];
                [request setPredicate:qsoPredicate];
                
                NSError *error = nil;
                noLotwQSOs = [moc executeFetchRequest:request error:&error];
                NSAssert(nil != noLotwQSOs,
                         @"Core Data failed to supply list of QSOs for export selection");
                [cell setEnabled:(0 < [noLotwQSOs count])];
            } else {
                // CAN'T HAPPEN
                NSAssert(false, @"button cell found in export panel that shouldn't be there");
            } // else
        } // for
    } // for
    
    panelResult = [sPanel runModalForDirectory:NSHomeDirectory()
                                          file:nil];
    if (NSOKButton == panelResult) {
        NSArray *exportQSOs = nil;
        if ([typeMatrix selectedCell] == logWindowController.exportAllButton) {
            exportQSOs = [[logWindowController qsosController] content];
        } else if ([typeMatrix selectedCell] == logWindowController.exportSelectedButton) {
            exportQSOs = [[logWindowController qsosController] selectedObjects];
        } else if ([typeMatrix selectedCell] == logWindowController.exportLoTWButton) {
            exportQSOs = noLotwQSOs;
        } else {
            // CAN'T HAPPEN
            NSAssert(false, @"button cell selected in export panel that shouldn't be there");
        } // else
        
        NSError *error = nil;
        BOOL result = [[[[QEADIFImporter alloc] init] autorelease] 
                       exportAsADIF:exportQSOs
                       toFilename:[sPanel filename] 
                       error:&error];
        if (!result && (nil != error)) {
            NSLog(@"  error on ADIF export");
        } // if
    } // if
} // doADIFExport

- (void)doADIFImport
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
	int result;
    
    self.mergeRequested = YES;
    NSArray *fileTypes = [NSArray arrayWithObjects:@"adi", @"ADI", @"adif", @"ADIF", nil];
    NSOpenPanel *oPanel = [NSOpenPanel openPanel];
	
    [oPanel setAllowsMultipleSelection:NO];
    [oPanel setAccessoryView:logWindowController.importPanelView];
    result = [oPanel runModalForDirectory:NSHomeDirectory()
									 file:nil types:fileTypes];
    if (result == NSOKButton) {        
		NSString *path = [[oPanel filenames] objectAtIndex:0];
        QEADIFFile *adifFile = [QEADIFFileMaker makeADIFFileFromFile:path error:nil];
        if (nil != adifFile) {
            NSLog(@"\nADIF IMPORT:\n%@\n\n", [adifFile dump]);
            NSArrayController *qsosController = [logWindowController qsosController];
            QEADIFImporter *importer = [[[QEADIFImporter alloc] init] autorelease];
            NSMutableSet *newRecords = [[[NSMutableSet alloc] init] autorelease];
            
            for (NSArray *record in [adifFile adifRecords]) {
                id tmpRecord = [importer importADIFRecord:record];
                if (nil == tmpRecord) {
                    NSLog(@"  adif record imported as nil");
                } else if (0 == [tmpRecord count]) {
                    NSLog(@"  adif record imported, but created no fields");
                } else {
                    NSLog(@"  record: %@", tmpRecord);
                    if (self.mergeRequested) {
                        NSLog(@"  attempting to merge record");
                    } else {
                        NSLog(@"  adding record without trying to merge");
                    } // else
                    
                    if (!self.mergeRequested || ![self mergeRecord:tmpRecord matches:newRecords]) {
                        id newEntity = [qsosController newObject];
                        for (NSString *columnName in [tmpRecord allKeys]) {
                            [newEntity setValue:[tmpRecord valueForKey:columnName] forKey:columnName];
                        } // for
                        [newRecords addObject:newEntity];
                    } // if
                } // else
            } // for
        } // if
    } else {
		NSLog(@"  import cancelled");
	} // else
} // doADIFImport


- (BOOL)mergeRecord:(NSDictionary *)record matches:(NSMutableSet *)matches
{
    NSLog(@"MESSAGE: %s", __PRETTY_FUNCTION__);
    
    // the following allocations and initializations are a prime target for optimization
    NSManagedObjectContext *moc = [self managedObjectContext];
    NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"QSO"
                                                         inManagedObjectContext:moc];
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:entityDescription];
    NSPredicate *qsoPredicateTemplate = [NSPredicate 
                                         predicateWithFormat:
                                         @"(callsign LIKE[c] $CALLSIGN) AND (band LIKE[c] $BAND) AND (mode LIKE[c] $MODE)"];
    
    NSDictionary *predicateValues = [NSDictionary 
                                     dictionaryWithObjectsAndKeys:
                                     [record valueForKey:@"callsign"], @"CALLSIGN",
                                     [record valueForKey:@"band"], @"BAND",
                                     [record valueForKey:@"mode"], @"MODE",
                                     nil];
    NSPredicate *qsoPredicate = [qsoPredicateTemplate
                                 predicateWithSubstitutionVariables:predicateValues];
    [request setPredicate:qsoPredicate];
    
    NSError *error = nil;
    NSArray *matchingQsos = [moc executeFetchRequest:request error:&error];
    if (nil == matchingQsos) {
        NSLog(@"  ERROR in fetching matching QSOs for merge: %@",
              [[error userInfo] valueForKey:NSLocalizedDescriptionKey]);
        return NO;
    } // if
    
    NSLog(@"  found %lu candidates for merging", \
          ((unsigned long) [matchingQsos count]));
    
    if (0 == [matchingQsos count]) return NO;
    
    QSO *closestMatch = nil;
    double closestInterval = DBL_MAX;
    for (QSO *candidate in matchingQsos) {
        NSAssert(nil != candidate.qso_date,
                 @"match candidate has nil qso_date");
        NSAssert(nil != [record valueForKey:@"qso_date"],
                 @"input record for merge has nil qso_date");
        if ([matches containsObject:candidate]) {
            NSLog(@"  rejecting candidate as new or already-merged record");
        } else if ((nil == closestMatch) && 
                   (fabs([candidate.qso_date timeIntervalSinceDate:
                          [record valueForKey:@"qso_date"]]) < MAX_MERGE_SECONDS)) {
            NSLog(@"  found initial candidate with interval of %.1f sec.", \
                  [candidate.qso_date timeIntervalSinceDate: \
                   [record valueForKey:@"qso_date"]]);
            closestMatch = candidate;
            closestInterval = fabs([candidate.qso_date timeIntervalSinceDate:
                                    [record valueForKey:@"qso_date"]]);
        } else if ((nil != closestMatch) &&
                   (fabs([candidate.qso_date timeIntervalSinceDate:
                          [record valueForKey:@"qso_date"]]) < closestInterval)) {
            NSLog(@"  found new candidate with interval of %.1f sec.", \
                  [candidate.qso_date timeIntervalSinceDate:\
                   [record valueForKey:@"qso_date"]]);
            closestMatch = candidate;
            closestInterval = fabs([candidate.qso_date timeIntervalSinceDate:
                                    [record valueForKey:@"qso_date"]]);
        } // else if
        else {
            NSLog(@"  rejecting candidate with interval of %.1f sec.", \
                  [candidate.qso_date timeIntervalSinceDate:\
                   [record valueForKey:@"qso_date"]]);
        } // else (DEBUG)
    } // for
    
    if (nil == closestMatch) {
        NSLog(@"  no match selected");
        return NO;
    } // if
    
    // merge record into candidate
    NSLog(@"  matched record (%.1f secs) for attempted merge:\n%@", closestInterval, closestMatch);
    [matches addObject:closestMatch];
    
    NSArray *colNames = [record allKeys];
    for (NSString *colName in colNames) {
        if (nil == [closestMatch valueForKey:colName]) {
            NSLog(@"    setting column %@; value: %@", colName, [record valueForKey:colName]);
            [closestMatch setValue:[record valueForKey:colName] forKey:colName];
        } else if (nil != [qslColumns member:colName]) {
            // For QSL data, we don't use nil for unset values but we instead
            // default to NO.  On the assumption that un-QSLing is relatively
            // rare, we assume when merging that YES always overwrites a NO.
            [closestMatch setValue:[NSNumber numberWithBool:
                                    ([[closestMatch valueForKey:colName] boolValue] ||
                                     [[record valueForKey:colName] boolValue])]
                            forKey:colName];
        } else if ([@"preservedFields" isEqualToString:colName]) {
            // we just concatenate on merge, on the assumption that the importer and
            // the parser won't choke on duplicate fields.
            closestMatch.preservedFields = [closestMatch.preservedFields 
                                            stringByAppendingString:[record valueForKey:@"preservedFields"]];
        }// else if
    } // for
    
    return YES;
} // mergeRecord:
@end
